package cn.tedu.lbs.article.web.service;

import cn.tedu.lbs.article.web.repository.ActionESRepository;
import cn.tedu.lbs.article.web.repository.ArticleESRepository;
import cn.tedu.lbs.article.web.util.AuthenticationContextUtil;
import cn.tedu.lbs.marketing.api.MarketingApi;
import com.alibaba.fastjson2.JSON;
import com.tarena.lbs.base.common.utils.Asserts;
import com.tarena.lbs.base.protocol.exception.BusinessException;
import com.tarena.lbs.base.protocol.pager.PageResult;
import com.tarena.lbs.common.content.utils.SequenceGenerator;
import com.tarena.lbs.common.passport.principle.UserPrinciple;
import com.tarena.lbs.pojo.content.entity.ActionSearchEntity;
import com.tarena.lbs.pojo.content.entity.ArticleSearchEntity;
import com.tarena.lbs.pojo.content.param.ArticleActionParam;
import com.tarena.lbs.pojo.content.param.ArticleContentParam;
import com.tarena.lbs.pojo.content.query.ArticleQuery;
import com.tarena.lbs.pojo.content.vo.ArticleActionPageResultVO;
import com.tarena.lbs.pojo.content.vo.ArticleActionVO;
import com.tarena.lbs.pojo.content.vo.ArticleVO;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.DubboReference;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.unit.DistanceUnit;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.GeoDistanceQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.collapse.CollapseBuilder;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;

@Service
@Slf4j
public class ArticleService {
    @Autowired
    private RestHighLevelClient esClient;
    @Autowired
    private ArticleESRepository esRepository;
    @Autowired
    private ActionESRepository actionESRepository;
    //如果 使用redis的客户端 操作的是数字类型数据 增加和减少 ,必须保证客户端底层序列化使用的是String序列化
    //不能使用 Object或者java 或者json
    @Autowired
    private StringRedisTemplate redisTemplate;
    public PageResult<ArticleVO> pageList(ArticleQuery articleQuery) throws BusinessException {
        //1.准备一个 具备 pageNo pageSize total属性的page对象唯独缺少ArticleVO列表
        PageResult<ArticleVO> pageResult=initPage();
        //2.根据前台 后台查询入口 给articleQuery计算封装完整条件值
        putAllQuery(articleQuery);
        //3.调用es的客户端对象 根据封装好的articleQuery使用非空属性 构造bool条件实现查询 返回vo
        List<ArticleVO> vos= searchArticles(articleQuery);
        pageResult.setObjects(vos);
        return pageResult;
    }

    private List<ArticleVO> searchArticles(ArticleQuery articleQuery) {
        //1.准备一个底层客户端RestHighLevelClient完整搜索流程代码 根据入参条件封装bool
        try{
            //2.提供一个请求对象
            SearchRequest request = new SearchRequest("lbs_article");
            //3.请求参数构建器
            SearchSourceBuilder builder= new SearchSourceBuilder();
            BoolQueryBuilder query=assembleBoolQuery(articleQuery);
            builder.query(query);
            request.source(builder);
            log.info("搜索文章列表的query:{}",builder);
            //4.客户端对象发送请求
            SearchResponse response = esClient.search(request, RequestOptions.DEFAULT);
            return parseResponse(response);
        }catch (Exception e){
            log.error("搜索查询文章列表异常",e);
        }
        return null;
    }

    private List<ArticleVO> parseResponse(SearchResponse response) {
        SearchHit[] docs = response.getHits().getHits();
        //文章存储的对象 ArticleSearchEntity 文档实际数据结构是json
        //从文档对象解析 source 直接转化成vo 前提是命中一批文档数据
        List<ArticleVO> vos=null;
        if (docs!=null&&docs.length>0){
            //命中文档对象 json获取 转化vo 处理留
            vos= Arrays.stream(docs).map(
                    doc->{
                        //从文档获取json fastjson工具转化对象vo
                        String json = doc.getSourceAsString();
                        return JSON.parseObject(json,ArticleVO.class);
                    }
            ).collect(Collectors.toList());
        }
        return vos;
    }

    private BoolQueryBuilder assembleBoolQuery(ArticleQuery articleQuery) {
        //可以关注非空属性 构造的bool字条件
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
        //1.文章标题 articleTitle 非空 matchQuery
        if (articleQuery.getArticleTitle()!=null){
            boolQuery.must(QueryBuilders.matchQuery("articleTitle", articleQuery.getArticleTitle()));
        }
        //2.后台查询 userId非空 查询某个作者的文章 termQuery
        if (articleQuery.getUserId()!=null){
            boolQuery.must(QueryBuilders.termQuery("userId", articleQuery.getUserId()));
        }
        //3.后台查询 从status数组中 传递一个文章状态 0草稿 1上线 termQuery
        if (articleQuery.getArticleStatus()!=null&&articleQuery.getArticleStatus().size()>0){
            //取出第一个元素 因为只有一个 termQuery查询
            Integer status = articleQuery.getArticleStatus().get(0);
            boolQuery.must(QueryBuilders.termQuery("articleStatus", status));
        }
        //4.前后台都要使用的 后台查询 查询的就只能是source=2 前台查询 查询 source=1||2
        //5.地理位置 如果入参有lat lon
        if (articleQuery.getLatitude()!=null&&articleQuery.getLongitude()!=null){
            GeoDistanceQueryBuilder location = QueryBuilders.geoDistanceQuery("location");
            location.point(
                    Double.valueOf(articleQuery.getLatitude()),
                    Double.valueOf(articleQuery.getLongitude()));
            location.distance(100d, DistanceUnit.KILOMETERS);
            boolQuery.must(location);
        }
        //6.前台查询条件 选择的文章标签数据
        if (articleQuery.getArticleLabel()!=null){
            boolQuery.must(QueryBuilders.termQuery("articleLabel", articleQuery.getArticleLabel()));
        }
        return boolQuery;
    }

    private void putAllQuery(ArticleQuery articleQuery) throws BusinessException {
        //在articleQuery查询条件里 有一个字段source=1前台查询 source=2后台查询
        Integer source = articleQuery.getSource();
        if (source==1){
            //小程序业务 暂时不考虑 小程序端查询文档 都是上线的文章 地理定位
            //上线的文章 status=1
            articleQuery.setArticleStatus(Arrays.asList(1));
        }else{
            //source=2 需要给条件补充一个数据 userId 表示文章作者需要认证流程
            UserPrinciple userPrinciple = AuthenticationContextUtil.get();
            Asserts.isTrue(userPrinciple==null,new BusinessException("-2","用户认证解析失败"));
            articleQuery.setUserId(userPrinciple.getId());
        }
    }

    private PageResult<ArticleVO> initPage() {
        PageResult<ArticleVO> page=new PageResult<>(10,1);
        page.setTotal(100L);
        return page;
    }

    public void save(ArticleContentParam articleContentParam) {
        //1.将param转化成entity 能拷贝 就拷贝 其他手动补充
        ArticleSearchEntity entity=new ArticleSearchEntity();
        BeanUtils.copyProperties(articleContentParam,entity);
        //1.1文档id 通过一个序列化器生成 唯一 递增趋势(有排序) 最好做到格式统一
        entity.setId(SequenceGenerator.sequence());
        //1.2补充作者 userId articleUserName
        UserPrinciple userPrinciple = AuthenticationContextUtil.get();
        entity.setUserId(userPrinciple.getId());
        entity.setArticleUsername(userPrinciple.getNickname());
        //1.3时间
        entity.setCreateTime(new Date());
        entity.setUpdateTime(new Date());
        //1.4 地理位置格式 把纬度 经度转化成字符串连接
        entity.setLocation(articleContentParam.getLatitude()+","+articleContentParam.getLongitude());
        //1.5 记录文章统计数字 点赞 访问 收藏 转发 评论等
        esRepository.save(entity);
    }

    public Set<String> getArticleLabels(ArticleQuery articleQuery) {
        //方案1: 只关心 查询搜索文章的逻辑 比如搜到1500篇文章 使用集合做去重
        //方案2: 查询搜索文章的时候 通过去重 得到的结果就是可以筛选的标签
        //1.组织查询条件 搜索一个去重的文章集合 每篇文章对象的标签都是唯一的
        articleQuery.setArticleStatus(Arrays.asList(1));
        List<ArticleSearchEntity> labelEntities=searchLabelArticles(articleQuery);
        //2.将所有的文章实体 组织转化成set集合
        Set<String> labels=assembleLabels(labelEntities);
        return labels;
    }

    private Set<String> assembleLabels(List<ArticleSearchEntity> labelEntities) {
        if (labelEntities!=null&&labelEntities.size()>0){
            return labelEntities.stream().map(entity->{
                return entity.getArticleLabel();
            }).collect(Collectors.toSet());
        }else{
            return null;
        }
    }

    private List<ArticleSearchEntity> searchLabelArticles(ArticleQuery articleQuery) {
        try{
            //1.request对象
            SearchRequest request=new SearchRequest("lbs_article");
            //2.组织请求参数 交给request
            SearchSourceBuilder sourceBuilder=new SearchSourceBuilder();
            //复用之前 做文章搜索列表 查询的封装bool条件的方法 保证query入参  满足现在接口需求的
            //articleQuery.setSource(null);
            BoolQueryBuilder query = assembleBoolQuery(articleQuery);
            sourceBuilder.query(query);
            sourceBuilder.collapse(new CollapseBuilder("articleLabel"));
            request.source(sourceBuilder);
            log.info("搜索文章标签集合的query:{}",sourceBuilder);
            //3.发送请求
            SearchResponse response = esClient.search(request, RequestOptions.DEFAULT);
            //4.解析响应 拿到entities
            SearchHit[] docs = response.getHits().getHits();
            if (docs!=null&&docs.length>0){
                return Arrays.stream(docs).map(doc->{
                    return JSON.parseObject(doc.getSourceAsString(),ArticleSearchEntity.class);
                }).collect(Collectors.toList());
            }
        }catch (Exception e){
            log.error("搜索文章标签异常",e);
        }
        return null;
    }
    //重构详情查询 2次
    //第一次: 携带活动
    //第二次: 读取统计数据 用户浏览文章 记录用户行为 因为用户浏览了文章 修改文章accessCount数字
    public ArticleVO detail(Integer id) throws BusinessException {
        //1.拿到认证解析的userId
        Integer userId=getUserId();
        //2.利用入参的docId 读取文章业务对象
        ArticleSearchEntity entity =esRepository.findById(id).get();//文档对象的解析结果
        //3.封装 用户访问该文章的行为数据 behavior 类型1=点赞 2=收藏 3=访问浏览
        saveUserActions(userId,entity,3);
        //4.考虑文章accessCount 增加一次 在redis中更新 点赞和收藏的复用
        behaveCountAdd(id,3,1);//对谋篇文章 某个行为 做原值自增1的操作
        //Integer currentAccessCount=(entity.getAccessCount()==null?0:entity.getAccessCount());
        //更新es中,文章的accessCount
        //updateArticleAccessCount(id,++currentAccessCount);
        //3.判断当前文章是否携带了活动id
        if (entity!=null&&entity.getActivityId()!=null){
            log.info("当前文章:{},携带活动id:{}",id,entity.getActivityId());
            //询问marketing当前文章的activityId是否给用户展示
            boolean visible=activityIsVisible(userId,entity.getActivityId());
            if (!visible){
                log.info("当前文章:{},携带活动id:{},不可见",id,entity.getActivityId());
                entity.setActivityId(null);
            }
        }
        ArticleVO articleVO = new ArticleVO();
        BeanUtils.copyProperties(entity, articleVO);
        //判断 activityId是否为空 如果不空 则调用marketing
        //重新查询依次当前文章所有数据统计数字 包括 浏览点赞 收藏
        putCounts(articleVO);
        return articleVO;
    }

    private void saveUserActions(Integer userId,ArticleSearchEntity entity,Integer behavior) {
        ActionSearchEntity action=new ActionSearchEntity();
        action.setBehaviorUserId(userId);//发起行为的用户 浏览文章的用户id
        action.setArticleId(entity.getId());
        action.setArticleTitle(entity.getArticleTitle());
        action.setBehavior(behavior);
        action.setCreateTime(new Date());
        action.setArticleUserId(entity.getUserId());
        action.setArticleLabel(entity.getArticleLabel());
        actionESRepository.save(action);
    }

    private void putCounts(ArticleVO articleVO) {
        //每次查询一个文章详情 ,都要把redis中存储的滇藏 收藏 访问数据量count查询出来 给vo补充
        HashOperations<String, String, String> opsForHash = redisTemplate.opsForHash();
        //如果没人点赞 没人收藏 没人访问 从redis读出来的数据是空 按照0处理
        String like = opsForHash.get("lbs:article:count:" + articleVO.getId(), 1 + "");//点赞次数
        String favor=opsForHash.get("lbs:article:count:"+articleVO.getId(),2+"");//收藏次数
        String access=opsForHash.get("lbs:article:count:"+articleVO.getId(),3+"");//浏览次数
        articleVO.setLikeCount(like==null?0:Integer.parseInt(like));
        articleVO.setFavoriteCount(favor==null?0:Integer.parseInt(favor));
        articleVO.setAccessCount(access==null?0:Integer.parseInt(access));
    }

    private void behaveCountAdd(Integer articleId, Integer behave, Integer count) {
        //hincrby key field 1
        HashOperations<String, String, String> opsForHash = redisTemplate.opsForHash();
        //key就是 绑定文章id的字符串
        String articleCountKey="lbs:article:count:"+articleId;
        opsForHash.increment(articleCountKey,behave+"",1);
    }

    @DubboReference
    private MarketingApi marketingApi;
    private boolean activityIsVisible(Integer userId, Integer activityId) {
        //设计一个远程调用的dubbo实现接口 完成判断
        return marketingApi.activityIsVisible(userId, activityId);
    }

    private Integer getUserId() throws BusinessException {
        UserPrinciple userPrinciple = AuthenticationContextUtil.get();
        Asserts.isTrue(userPrinciple==null,new BusinessException("-2","用户认证解析失败"));
        return userPrinciple.getId();
    }

    public void behave(ArticleActionParam articleActionParam) throws BusinessException {
        //1.拿到用户身份
        Integer userId = getUserId();
        //2.利用文章id拿到文章详情
        ArticleSearchEntity entity = esRepository.findById(articleActionParam.getId()).get();
        //3.封装点赞收藏的行为对象
        saveUserActions(userId,entity,articleActionParam.getBehavior());
        //4.记录count计步器
        behaveCountAdd(entity.getId(),articleActionParam.getBehavior(),1);
    }

    public ArticleActionPageResultVO getBehaveList() throws BusinessException, ExecutionException, InterruptedException {
        //1.解析认证对象的userId
        Integer userId = getUserId();
        //2.查询 该用户 曾经点赞 behave=1的文章
        CompletableFuture<PageResult<ArticleActionVO>> likeFuture=CompletableFuture.supplyAsync(()->{
            return selectBehaveArticles(userId,1);
        });
        //3.查询 该用户 曾经收藏 behave=2的文章
        CompletableFuture<PageResult<ArticleActionVO>> favorFuture=CompletableFuture.supplyAsync(()->{
            return selectBehaveArticles(userId,2);
        });
        //4.查询 该用户 曾经流浪访问过的文章 behave=3
        CompletableFuture<PageResult<ArticleActionVO>> accessFuture=CompletableFuture.supplyAsync(()->{
            return selectBehaveArticles(userId,3);
        });
        //阻塞等待所有 future拿到 反馈结果
        CompletableFuture<Void> allOf = CompletableFuture.allOf(likeFuture, favorFuture, accessFuture);
        //5.封装VO 返回
        ArticleActionPageResultVO vo=new ArticleActionPageResultVO();
        vo.setLikeFuturePageResult(likeFuture.get());
        vo.setCollectFutureOPageResult(favorFuture.get());
        vo.setCommentFuturePageResult(accessFuture.get());
        return vo;
    }

    private PageResult<ArticleActionVO> selectBehaveArticles(Integer userId, Integer behave) {
        //1.搜索查询请求对象 lbs_action ActionSearchEntity
        SearchRequest request=new SearchRequest("lbs_action");
        //2.封装提供查询请求参数
        SearchSourceBuilder sourceBuilder=new SearchSourceBuilder();
        BoolQueryBuilder query = QueryBuilders.boolQuery();
        //2.1termQuery 当前用户
        query.must(QueryBuilders.termQuery("behaviorUserId",userId));
        //2.2termQuery 用户行为类型
        query.must(QueryBuilders.termQuery("behavior",behave));
        //2.3articleId去重
        sourceBuilder.query(query);
        sourceBuilder.collapse(new CollapseBuilder("articleId"));
        request.source(sourceBuilder);
        //3.发起查询请求
        List<ActionSearchEntity> entities=null;
        try{
            SearchResponse response=esClient.search(request,RequestOptions.DEFAULT);
            //4.封装返回结果
            SearchHit[] docs = response.getHits().getHits();
            if (docs!=null&&docs.length>0){
                entities=Arrays.stream(docs).map(doc->{
                    return JSON.parseObject(doc.getSourceAsString(),ActionSearchEntity.class);
                }).collect(Collectors.toList());
            }
        }catch (Exception e){
            log.error("查询用户行为异常",e);
        }
        //5.转化一下对象 装配到pageResult中
        return assemblePageResult(entities);
    }

    private PageResult<ArticleActionVO> assemblePageResult(List<ActionSearchEntity> entities) {
        PageResult<ArticleActionVO> pageVos=new PageResult<>(10,1);
        pageVos.setTotal(100L);
        List<ArticleActionVO> vos=null;
        if (entities!=null&&entities.size()>0){
            vos=entities.stream().map(entity->{
                ArticleActionVO vo=new ArticleActionVO();
                vo.setArticleId(entity.getArticleId());
                vo.setArticleTitle(entity.getArticleTitle());
                vo.setCreateTime(entity.getCreateTime().toString());
                vo.setComment(entity.getComment());
                //每一个经过行为记录 查询的数据需要文章封面
                vo.setArticleCover(esRepository.findById(entity.getArticleId()).get().getArticleCover());
                return vo;
            }).collect(Collectors.toList());
        }
        pageVos.setObjects(vos);
        return pageVos;
    }
}
